/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.search;

import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc; //Issue 1726
import org.apache.lucene.search.Sort;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.MapSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.request.SolrQueryRequest;

import java.util.*;

/**
 * <b>Note: This API is experimental and may change in non backward-compatible
 * ways in the future</b>
 * 
 * 
 */
public abstract class QParser {
	protected String qstr;
	protected SolrParams params;
	protected SolrParams localParams;
	protected SolrQueryRequest req;
	protected int recurseCount;

	protected Query query;

	protected String stringIncludingLocalParams; // the original query string
													// including any local
													// params
	protected boolean valFollowedParams; // true if the value "qstr" followed
											// the localParams
	protected int localParamsEnd; // the position one past where the localParams
									// ended

	/**
	 * Constructor for the QParser
	 * 
	 * @param qstr
	 *            The part of the query string specific to this parser
	 * @param localParams
	 *            The set of parameters that are specific to this QParser. See
	 *            http://wiki.apache.org/solr/LocalParams
	 * @param params
	 *            The rest of the
	 *            {@link org.apache.solr.common.params.SolrParams}
	 * @param req
	 *            The original {@link org.apache.solr.request.SolrQueryRequest}.
	 */
	public QParser(String qstr, SolrParams localParams, SolrParams params,
			SolrQueryRequest req) {
		this.qstr = qstr;
		this.localParams = localParams;

		// insert tags into tagmap.
		// WARNING: the internal representation of tagged objects in the request
		// context is
		// experimental and subject to change!
		if (localParams != null) {
			String tagStr = localParams.get(CommonParams.TAG);
			if (tagStr != null) {
				Map<Object, Object> context = req.getContext();
				@SuppressWarnings("unchecked")
				Map<Object, Collection<Object>> tagMap = (Map<Object, Collection<Object>>) req
						.getContext().get("tags");
				if (tagMap == null) {
					tagMap = new HashMap<Object, Collection<Object>>();
					context.put("tags", tagMap);
				}
				if (tagStr.indexOf(',') >= 0) {
					List<String> tags = StrUtils.splitSmart(tagStr, ',');
					for (String tag : tags) {
						addTag(tagMap, tag, this);
					}
				} else {
					addTag(tagMap, tagStr, this);
				}
			}
		}

		this.params = params;
		this.req = req;
	}

	private static void addTag(Map<Object, Collection<Object>> tagMap,
			Object key, Object val) {
		Collection<Object> lst = tagMap.get(key);
		if (lst == null) {
			lst = new ArrayList<Object>(2);
			tagMap.put(key, lst);
		}
		lst.add(val);
	}

	/**
	 * Create and return the <code>Query</code> object represented by
	 * <code>qstr</code>. Null MAY be returned to signify there was no input
	 * (e.g. no query string) to parse.
	 * 
	 * @see #getQuery()
	 **/
	public abstract Query parse() throws ParseException;

	public SolrParams getLocalParams() {
		return localParams;
	}

	public void setLocalParams(SolrParams localParams) {
		this.localParams = localParams;
	}

	public SolrParams getParams() {
		return params;
	}

	public void setParams(SolrParams params) {
		this.params = params;
	}

	public SolrQueryRequest getReq() {
		return req;
	}

	public void setReq(SolrQueryRequest req) {
		this.req = req;
	}

	public String getString() {
		return qstr;
	}

	public void setString(String s) {
		this.qstr = s;
	}

	/**
	 * Returns the resulting query from this QParser, calling parse() only the
	 * first time and caching the Query result.
	 */
	public Query getQuery() throws ParseException {
		if (query == null) {
			query = parse();

			if (localParams != null) {
				String cacheStr = localParams.get(CommonParams.CACHE);
				if (cacheStr != null) {
					if (CommonParams.FALSE.equals(cacheStr)) {
						extendedQuery().setCache(false);
					} else if (CommonParams.TRUE.equals(cacheStr)) {
						extendedQuery().setCache(true);
					} else if ("sep".equals(cacheStr)) {
						extendedQuery().setCacheSep(true);
					}
				}

				int cost = localParams.getInt(CommonParams.COST,
						Integer.MIN_VALUE);
				if (cost != Integer.MIN_VALUE) {
					extendedQuery().setCost(cost);
				}
			}
		}
		return query;
	}

	// returns an extended query (and sets "query" to a new wrapped query if
	// necessary)
	private ExtendedQuery extendedQuery() {
		if (query instanceof ExtendedQuery) {
			return (ExtendedQuery) query;
		} else {
			WrappedQuery wq = new WrappedQuery(query);
			query = wq;
			return wq;
		}
	}

	private void checkRecurse() throws ParseException {
		if (recurseCount++ >= 100) {
			throw new ParseException(
					"Infinite Recursion detected parsing query '" + qstr + "'");
		}
	}

	// TODO: replace with a SolrParams that defaults to checking localParams
	// first?
	// ideas..
	// create params that satisfy field-specific overrides
	// overrideable syntax $x=foo (set global for limited scope) (invariants &
	// security?)
	// $x+=foo (append to global for limited scope)

	/** check both local and global params */
	public String getParam(String name) {
		String val;
		if (localParams != null) {
			val = localParams.get(name);
			if (val != null)
				return val;
		}
		return params.get(name);
	}

	/** Create a new QParser for parsing an embedded sub-query */
	public QParser subQuery(String q, String defaultType) throws ParseException {
		checkRecurse();
		if (defaultType == null && localParams != null) {
			// if not passed, try and get the defaultType from local params
			defaultType = localParams.get(QueryParsing.DEFTYPE);
		}
		QParser nestedParser = getParser(q, defaultType, getReq());
		nestedParser.recurseCount = recurseCount;
		recurseCount--;
		return nestedParser;
	}

	/**
	 * use common params to look up pageScore and pageDoc in global params
	 * 
	 * @return the ScoreDoc
	 */
	public ScoreDoc getPaging() throws ParseException {
		return null;

		/***
		 * This is not ready for prime-time... see SOLR-1726
		 * 
		 * String pageScoreS = null; String pageDocS = null;
		 * 
		 * pageScoreS = params.get(CommonParams.PAGESCORE); pageDocS =
		 * params.get(CommonParams.PAGEDOC);
		 * 
		 * if (pageScoreS == null || pageDocS == null) return null;
		 * 
		 * int pageDoc = pageDocS != null ? Integer.parseInt(pageDocS) : -1;
		 * float pageScore = pageScoreS != null ? new Float(pageScoreS) : -1;
		 * if(pageDoc != -1 && pageScore != -1){ return new ScoreDoc(pageDoc,
		 * pageScore); } else { return null; }
		 ***/
	}

	/**
	 * @param useGlobalParams
	 *            look up sort, start, rows in global params if not in local
	 *            params
	 * @return the sort specification
	 */
	public SortSpec getSort(boolean useGlobalParams) throws ParseException {
		getQuery(); // ensure query is parsed first

		String sortStr = null;
		String startS = null;
		String rowsS = null;

		if (localParams != null) {
			sortStr = localParams.get(CommonParams.SORT);
			startS = localParams.get(CommonParams.START);
			rowsS = localParams.get(CommonParams.ROWS);

			// if any of these parameters are present, don't go back to the
			// global params
			if (sortStr != null || startS != null || rowsS != null) {
				useGlobalParams = false;
			}
		}

		if (useGlobalParams) {
			if (sortStr == null) {
				sortStr = params.get(CommonParams.SORT);
			}
			if (startS == null) {
				startS = params.get(CommonParams.START);
			}
			if (rowsS == null) {
				rowsS = params.get(CommonParams.ROWS);
			}
		}

		int start = startS != null ? Integer.parseInt(startS) : 0;
		int rows = rowsS != null ? Integer.parseInt(rowsS) : 10;

		Sort sort = null;
		if (sortStr != null) {
			sort = QueryParsing.parseSort(sortStr, req);
		}
		return new SortSpec(sort, start, rows);
	}

	public String[] getDefaultHighlightFields() {
		return new String[] {};
	}

	public Query getHighlightQuery() throws ParseException {
		Query query = getQuery();
		return query instanceof WrappedQuery ? ((WrappedQuery) query)
				.getWrappedQuery() : query;
	}

	public void addDebugInfo(NamedList<Object> debugInfo) {
		debugInfo.add("QParser", this.getClass().getSimpleName());
	}

	/**
	 * Create a <code>QParser</code> to parse <code>qstr</code>, assuming that
	 * the default query parser is <code>defaultParser</code>. The query parser
	 * may be overridden by local parameters in the query string itself. For
	 * example if defaultParser=<code>"dismax"</code> and qstr=<code>foo</code>,
	 * then the dismax query parser will be used to parse and construct the
	 * query object. However if qstr=<code>{!prefix f=myfield}foo</code> then
	 * the prefix query parser will be used.
	 */
	public static QParser getParser(String qstr, String defaultParser,
			SolrQueryRequest req) throws ParseException {
		// SolrParams localParams = QueryParsing.getLocalParams(qstr,
		// req.getParams());

		String stringIncludingLocalParams = qstr;
		SolrParams localParams = null;
		SolrParams globalParams = req.getParams();
		boolean valFollowedParams = true;
		int localParamsEnd = -1;

		if (qstr != null && qstr.startsWith(QueryParsing.LOCALPARAM_START)) {
			Map<String, String> localMap = new HashMap<String, String>();
			localParamsEnd = QueryParsing.parseLocalParams(qstr, 0, localMap,
					globalParams);

			String val = localMap.get(QueryParsing.V);
			if (val != null) {
				// val was directly specified in localParams via v=<something>
				// or v=$arg
				valFollowedParams = false;
			} else {
				// use the remainder of the string as the value
				valFollowedParams = true;
				val = qstr.substring(localParamsEnd);
				localMap.put(QueryParsing.V, val);
			}
			localParams = new MapSolrParams(localMap);
		}

		String parserName;

		if (localParams == null) {
			parserName = defaultParser;
		} else {
			parserName = localParams.get(QueryParsing.TYPE, defaultParser);
			qstr = localParams.get("v");
		}

		parserName = parserName == null ? QParserPlugin.DEFAULT_QTYPE
				: parserName;

		QParserPlugin qplug = req.getCore().getQueryPlugin(parserName);
		QParser parser = qplug.createParser(qstr, localParams, req.getParams(),
				req);

		parser.stringIncludingLocalParams = stringIncludingLocalParams;
		parser.valFollowedParams = valFollowedParams;
		parser.localParamsEnd = localParamsEnd;
		return parser;
	}

}
